package com.xushuda.yas.storage;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by xushuda on 14/12/31. map the bitSet to file using mmap
 */
public class MmBit {

    protected int bitNum;

    protected int byteNum;

    protected MappedByteBuffer mappedByteBuffer;

    // the write operation is always synchronized, so we don't need ConcurrentHasMap
    protected static Map<String, MmBit> instances;

    protected static final Object lock = new Object();

    // use only double check to ensure the single instance in mem for every bloom filter file
    public static MmBit getInstance(String pathName, int bitNum) throws IOException {
        
        // if hasMap is not initialized
        if (null == instances) {
            // synchronize this class
            synchronized (lock) {
                if (null == instances)
                    instances = new HashMap<String, MmBit>();
            }
        }

        // check the value in instances
        if (!instances.containsKey(pathName)) {
            synchronized (lock) {
                if (!instances.containsKey(pathName))
                    instances.put(pathName, new MmBit(pathName, bitNum));
            }
        }

        return instances.get(pathName);

    }

    @Deprecated
    // the bitNum isn not exactly the real num
    public MmBit(String pathName) throws IOException {
        
        File file = new File(pathName);

        if (!file.exists()) {
            throw new IOException("file not exists");
        }

        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");

        FileChannel fileChannel = randomAccessFile.getChannel();

        long size = fileChannel.size();

        byteNum = (int) size;

        // this is not the correct bitNum
        bitNum = 8 * (byteNum - 1);

        mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);

        // close
        randomAccessFile.close();
        fileChannel.close();

    }

    @Deprecated
    // u should use getInstance to get instance
    public MmBit(String pathName, int bitNum) throws IOException {
        // num of bit
        this.bitNum = bitNum;
        // get num of bytes
        byteNum = (bitNum % 8 == 0) ? bitNum / 8 : bitNum / 8 + 1;
        // declare the variable
        RandomAccessFile randomAccessFile = null;
        FileChannel fileChannel = null;
        // get the file
        File file = new File(pathName);
        // file doesn't exist
        if (!file.exists()) {
            // if file doesn't exist , create one
            if (!file.createNewFile()) {
                throw new IOException("file create fail");
            }
            // access the file
            randomAccessFile = new RandomAccessFile(pathName, "rw");

            fileChannel = randomAccessFile.getChannel();
            // get the bit set
            BitSet bitSet = new BitSet(bitNum);

            byte[] data = bitSet.toByteArray();
            // get the byteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(byteNum);
            // put the bitSet into the byteBuffer
            byteBuffer.put(data);

            assert (byteNum == data.length);
            // reset the inner position of this byteBuffer
            byteBuffer.clear();
            // write the bytes into the file
            fileChannel.write(byteBuffer);

            // file exists
        } else {
            randomAccessFile = new RandomAccessFile(pathName, "rw");

            fileChannel = randomAccessFile.getChannel();
        }

        long size = fileChannel.size();

        if (size != byteNum) {
            randomAccessFile.close();
            fileChannel.close();
            throw new IOException("the file size is not equal to calculated byteNum");
        }

        mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);

        // close
        randomAccessFile.close();
        fileChannel.close();

    }

    public boolean get(int bitIndex) {

        if (bitIndex < 0 || bitIndex > bitNum)
            throw new IndexOutOfBoundsException("bitIndex < 0 or bitIndex out of boundary: " + bitIndex);

        int bytePosition = bitIndex >>> 3;

        int bitShift = (bitIndex & 0x7);

        byte byteValue = (byte) (1 << bitShift);

        byte currentValue = mappedByteBuffer.get(bytePosition);

        return 0 != (byteValue & currentValue);
    }

    public void clear(int bitIndex) {

        if (bitIndex < 0 || bitIndex > bitNum)
            throw new IndexOutOfBoundsException("bitIndex < 0 or bitIndex out of boundary: " + bitIndex);

        int bytePosition = bitIndex >>> 3;

        int bitShift = (bitIndex & 0x7);

        byte byteValue = (byte) ~(1 << bitShift);

        byte currentValue = mappedByteBuffer.get(bytePosition);

        mappedByteBuffer.put(bytePosition, (byte) ((currentValue | byteValue) & 0xff));

    }

    public void set(int bitIndex) {

        if (bitIndex < 0 || bitIndex > bitNum)
            throw new IndexOutOfBoundsException("bitIndex < 0 or bitIndex out of boundary: " + bitIndex);

        int bytePosition = bitIndex >>> 3;

        int bitShift = (bitIndex & 0x7);

        byte byteValue = (byte) (1 << bitShift);

        byte currentValue = mappedByteBuffer.get(bytePosition);

        mappedByteBuffer.put(bytePosition, (byte) ((currentValue | byteValue) & 0xff));

    }

    public void set(int bitIndex, boolean flag) {
        if (flag)
            set(bitIndex);
        else
            clear(bitIndex);
    }

    public int length() {
        return bitNum;
    }

}
